/* Copyright (c) 2013 OpenPlans - www.openplans.org. All rights reserved.
 * This code is licensed under the GPL 2.0 license, available at the root
 * application directory.
 */
package org.geoserver.catalog.impl;

import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.apache.commons.beanutils.ConstructorUtils;
import org.apache.commons.lang.SerializationUtils;
import org.geoserver.catalog.CatalogInfo;
import org.geoserver.config.util.XStreamPersister;
import org.geoserver.config.util.XStreamPersisterFactory;
import org.geotools.util.logging.Logging;

import com.thoughtworks.xstream.XStream;

/**
 * Utility class used to wrap/clone objects and collections by various strategies:
 * <ul>
 * <li>Avoid cloning ModificationProxy proxies, as well as any CatalogInfo object</li>
 * <li>Avoid cloning at all well known objects that are known to be immutable (several classes in
 * java.lang)</li>
 * <li>Wrap in ModificatinoProxy any object that is a CatalogInfo</li>
 * <li>Using {@link Cloneable} if available</li>
 * <li>Using copy constructors if available</li>
 * <li>Falling back on XStream serialization if the above fails
 * 
 * @author Andrea Aime - GeoSolutions
 * 
 */
class ModificationProxyCloner {

    private static final XStreamPersisterFactory XSTREAM_PERSISTER_FACTORY = new XStreamPersisterFactory();

    static final Logger LOGGER = Logging.getLogger(ModificationProxyCloner.class);
    
    static final Map<Class, Class> CATALOGINFO_INTERFACE_CACHE = new ConcurrentHashMap<Class, Class>();

    /**
     * Best effort object cloning utility, tries different lightweight strategies, then falls back
     * on copy by XStream serialization (we use that one as we have a number of hooks to avoid deep
     * copying the catalog, and re-attaching to it, in there)
     * 
     * @param source
     * @return
     */
    static <T> T clone(T source) {
        // null?
        if (source == null) {
            return null;
        }
        
        // already a modification proxy?
        if(ModificationProxy.handler(source) != null) {
            return source;
        }
        
        // is it a catalog info?
        if(source instanceof CatalogInfo) {
            // mumble... shouldn't we wrap this one in a modification proxy object?
            return (T) ModificationProxy.create(source, getDeepestCatalogInfoInterface((CatalogInfo) source));
        }

        // if a known immutable?
        if (source instanceof String || source instanceof Byte || source instanceof Short
                || source instanceof Integer || source instanceof Float || source instanceof Double
                || source instanceof BigInteger || source instanceof BigDecimal) {
            return (T) source;
        }

        // is it cloneable?
        try {
            if (source instanceof Cloneable) {
                // methodutils does not seem to work against "clone()"...
                // return (T) MethodUtils.invokeExactMethod(source, "clone", null, null);
                Method method = source.getClass().getDeclaredMethod("clone");
                if(Modifier.isPublic(method.getModifiers()) && method.getParameterTypes().length == 0) {
                    return (T) method.invoke(source);
                }
            }
        } catch (Exception e) {
            LOGGER.log(
                    Level.FINE,
                    "Source object is cloneable, yet it does not have a public no argument method 'clone'",
                    e);
        }

        // does it have a copy constructor?
        Constructor copyConstructor = ConstructorUtils.getAccessibleConstructor(source.getClass(),
                source.getClass());
        if (copyConstructor != null) {
            try {
                return (T) copyConstructor.newInstance(source);
            } catch (Exception e) {
                LOGGER.log(Level.FINE,
                        "Source has a copy constructor, but it failed, skipping to XStream", e);
            }
        }
        

        if(source instanceof Serializable) {
            return (T) SerializationUtils.clone((Serializable) source);
        } else {
            XStreamPersister persister = XSTREAM_PERSISTER_FACTORY.createXMLPersister();
            XStream xs = persister.getXStream();
            String xml = xs.toXML(source);
            T copy = (T) xs.fromXML(xml);
            return copy;
        }
    }
    
    static Class getDeepestCatalogInfoInterface(CatalogInfo object) {
        Class<? extends CatalogInfo> sourceClass = object.getClass();
        Class result = CATALOGINFO_INTERFACE_CACHE.get(sourceClass);
        if(result == null) {
            Class[] interfaces = sourceClass.getInterfaces();
            // collect only CatalogInfo related interfaces
            List<Class> cis = new ArrayList<Class>();
            for (Class clazz : interfaces) {
                if(CatalogInfo.class.isAssignableFrom(clazz)) {
                    cis.add(clazz);
                }
            }
            if(cis.size() == 0) {
                result = null;
            } else if(cis.size() == 1) {
                result = cis.get(0);
            } else {
                Collections.sort(cis, new Comparator<Class>() {
        
                    @Override
                    public int compare(Class c1, Class c2) {
                        if(c1.isAssignableFrom(c2)) {
                            return 1;
                        } else if(c2.isAssignableFrom(c1)) {
                            return -1;
                        } else {
                            return 0;
                        }
                    }
                });
            
                result = cis.get(0);
            }
            
            CATALOGINFO_INTERFACE_CACHE.put(sourceClass, result);
        }
        
        return result;
        
        
    }

    /**
     * Shallow or deep copies the provided collection
     * 
     * @param source
     * @param deepCopy If true, a deep copy will be done, otherwise the cloned collection will
     *        contain the exact same objects as the source
     * @return
     * @throws InstantiationException
     * @throws IllegalAccessException
     */
    public static <T> Collection<T> cloneCollection(Collection<T> source, boolean deepCopy)
            throws InstantiationException, IllegalAccessException {
        if (source == null) {
            // nothing to copy
            return null;
        }
        Collection<T> copy = source.getClass().newInstance();
        if (deepCopy) {
            for (T object : source) {
                T objectCopy = clone(object);
                copy.add(objectCopy);
            }
        } else {
            copy.addAll(source);
        }

        return copy;
    }

    /**
     * Shallow or deep copies the provided collection
     * 
     * @param <K>
     * @param <V>
     * 
     * @param source
     * @param deepCopy If true, a deep copy will be done, otherwise the cloned collection will
     *        contain the exact same objects as the source
     * @return
     * @throws InstantiationException
     * @throws IllegalAccessException
     */
    public static <K, V> Map<K, V> cloneMap(Map<K, V> source, boolean deepCopy)
            throws InstantiationException, IllegalAccessException {
        if (source == null) {
            // nothing to copy
            return null;
        }
        Map<K, V> copy = source.getClass().newInstance();
        if (deepCopy) {
            for (Map.Entry<K, V> entry : source.entrySet()) {
                K keyCopy = clone(entry.getKey());
                V valueCopy = clone(entry.getValue());
                copy.put(keyCopy, valueCopy);
            }
        } else {
            copy.putAll(source);
        }

        return copy;
    }

}
